/**********************************************************************

 Audacity: A Digital Audio Editor

 RealtimeEffectManager.h

 Paul Licameli split from EffectManager.h

 **********************************************************************/

#ifndef __AUDACITY_REALTIME_EFFECT_MANAGER__
#define __AUDACITY_REALTIME_EFFECT_MANAGER__

#include <atomic>
#include <chrono>
#include <memory>
#include <mutex>
#include <optional>
#include <unordered_map>
#include <vector>

#include "ClientData.h"
#include "Observer.h"
#include "PluginProvider.h" // for PluginID
#include "RealtimeEffectList.h"

class ChannelGroup;
class EffectInstance;

namespace RealtimeEffects {
class InitializationScope;
class ProcessingScope;
}

///Posted when effect is being added or removed to/from channel group or project
struct RealtimeEffectManagerMessage
{
    enum class Type
    {
        EffectAdded,
        EffectReplaced,
        EffectRemoved
    };
    Type type{};
    ChannelGroup* group{}; ///< null, if changes happened in the project scope
};

class REALTIME_EFFECTS_API RealtimeEffectManager final : public ClientData::Base, public Observer::Publisher<RealtimeEffectManagerMessage>
{
public:
    //!Special value used to identify special effects stack applied
    //!to every playable track
    static constexpr ChannelGroup* MasterGroup = nullptr;

    using Latency = std::chrono::microseconds;

    RealtimeEffectManager(AudacityProject& project);
    ~RealtimeEffectManager();

    static RealtimeEffectManager& Get(AudacityProject& project);
    static const RealtimeEffectManager& Get(const AudacityProject& project);

    // Realtime effect processing

    //! To be called only from main thread
    bool IsActive() const noexcept;
//   Latency GetLatency() const;

    //! Main thread appends a global or per-group effect
    /*!
     @param pScope if realtime is active but scope is absent, there is no effect
     @param pGroup if null, then state is added to the global list
     @param id identifies the effect
     @return if null, the given id was not found

     @post result: `!result || result->GetEffect() != nullptr`
     */
    std::shared_ptr<RealtimeEffectState> AddState(
        RealtimeEffects::InitializationScope* pScope, ChannelGroup* pGroup, const PluginID& id);

    //! Main thread replaces a global or per-group effect
    /*!
     @param pScope if realtime is active but scope is absent, there is no effect
     @param pGroup if null, then state is added to the global list
     @param index position in the list to replace; no effect if out of range
     @param id identifies the effect
     @return if null, the given id was not found and the old state remains

     @post result: `!result || result->GetEffect() != nullptr`
     */
    std::shared_ptr<RealtimeEffectState> ReplaceState(
        RealtimeEffects::InitializationScope* pScope, ChannelGroup* pGroup, size_t index, const PluginID& id);

    //! Main thread removes a global or per-group effect
    /*!
     @param pScope if realtime is active but scope is absent, there is no effect
     @param pGroup if null, then state is added to the global list
     @param state the state to be removed
     */
    /*! No effect if realtime is active but scope is not supplied */
    void RemoveState(RealtimeEffects::InitializationScope* pScope, ChannelGroup* pGroup, std::shared_ptr<RealtimeEffectState> pState);

    //! Report the position of a state in the global or a per-group list
    std::optional<size_t> FindState(
        ChannelGroup* pGroup, const std::shared_ptr<RealtimeEffectState>& pState) const;

    bool GetSuspended() const
    { return mSuspended.load(std::memory_order_relaxed); }

    //! To be called only from main thread
    /*!
     Each time a processing scope starts in the audio thread, suspension state
     is tested, and plug-in instances may need to do things in response to the
     transitions.  Playback of samples may continue but with effects switched
     off in suspended state.
     */
    void SetSuspended(bool value)
    { mSuspended.store(value, std::memory_order_relaxed); }

private:
    friend RealtimeEffects::InitializationScope;

    std::shared_ptr<RealtimeEffectState>
    MakeNewState(RealtimeEffects::InitializationScope* pScope, ChannelGroup* pGroup, const PluginID& id);

    //! Main thread begins to define a set of groups for playback
    void Initialize(RealtimeEffects::InitializationScope& scope, unsigned numPlaybackChannels, double sampleRate,
                    size_t audioThreadBufferSize);
    //! Main thread adds one group (passing the first of one or more
    //! channels), still before playback
    void AddGroup(RealtimeEffects::InitializationScope& scope, const ChannelGroup& group, unsigned chans, float rate,
                  size_t audioThreadBufferSize);
    //! Main thread cleans up after playback
    void Finalize() noexcept;

    friend RealtimeEffects::ProcessingScope;

    void ProcessStart(bool suspended);

    /*! @copydoc ProcessScope::Process */
    size_t Process(bool suspended, const ChannelGroup* group, float* const* buffers, float* const* scratch, float* dummy, unsigned nBuffers,
                   size_t numSamples);
    void ProcessEnd(bool suspended) noexcept;

    RealtimeEffectManager(const RealtimeEffectManager&) = delete;
    RealtimeEffectManager& operator=(const RealtimeEffectManager&) = delete;

    // Type that state visitor functions would have for out-of-line definition
    // of VisitGroup and VisitAll
    // using StateVisitor =
    // std::function<void(RealtimeEffectState &state, bool listIsActive)> ;

    //! Visit states for group or for the master when group is null
    template<typename StateVisitor>
    void VisitGroup(ChannelGroup* group, const StateVisitor& func)
    {
        if (group == nullptr) {
            RealtimeEffectList::Get(mProject).Visit(func);
        } else {
            // Call the function for each effect on the group list
            RealtimeEffectList::Get(*group).Visit(func);
        }
    }

    template<typename StateVisitor>
    void VisitGroup(const ChannelGroup* group, const StateVisitor& func)
    {
        VisitGroup(const_cast<ChannelGroup*>(group), func);
    }

    //! Visit the per-project states first, then all groups from AddGroup
    /*! Groups are visited in unspecified order */
    template<typename StateVisitor>
    void VisitAll(const StateVisitor& func)
    {
        // Call the function for each effect on the master list
        RealtimeEffectList::Get(mProject).Visit(func);

        // And all group lists
        for (auto group : mGroups) {
            RealtimeEffectList::Get(*group).Visit(func);
        }
    }

    AudacityProject& mProject;
    //Latency mLatency{ 0 };

    std::atomic<bool> mSuspended{ true };

    bool mActive{ false };

    // This member is mutated only by Initialize(), AddGroup(), Finalize()
    // which are to be called only while there is no playback
    std::vector<const ChannelGroup*> mGroups; //!< all are non-null

    std::unordered_map<const ChannelGroup*, double> mRates;
};

namespace RealtimeEffects {
//! Brackets processing setup and cleanup in the main thread
class InitializationScope
{
public:
    InitializationScope() {}
    explicit InitializationScope(
        std::weak_ptr<AudacityProject> wProject, double sampleRate,
        unsigned numPlaybackChannels, size_t audioThreadBufferSize)
        : mSampleRate{sampleRate}
        , mwProject{move(wProject)}
        , mNumPlaybackChannels{numPlaybackChannels}
        , mAudioThreadBufferSize{audioThreadBufferSize}
    {
        if (const auto pProject = mwProject.lock()) {
            RealtimeEffectManager::Get(*pProject).Initialize(*this, numPlaybackChannels, sampleRate, audioThreadBufferSize);
        }
    }

    InitializationScope(InitializationScope&& other) = default;
    InitializationScope& operator=(InitializationScope&& other) = default;
    ~InitializationScope()
    {
        if (auto pProject = mwProject.lock()) {
            RealtimeEffectManager::Get(*pProject).Finalize();
        }
    }

    void AddGroup(const ChannelGroup& group,
                  unsigned chans, float rate, size_t audioThreadBufferSize)
    {
        if (auto pProject = mwProject.lock()) {
            RealtimeEffectManager::Get(*pProject)
            .AddGroup(*this, group, chans, rate, audioThreadBufferSize);
        }
    }

    std::vector<std::shared_ptr<EffectInstance> > mInstances;
    double mSampleRate;
    unsigned mNumPlaybackChannels;
    size_t mAudioThreadBufferSize;

private:
    std::weak_ptr<AudacityProject> mwProject;
};

//! Brackets one block of processing in one thread
class ProcessingScope
{
public:
    //! Require a prior InializationScope to ensure correct nesting
    explicit ProcessingScope(InitializationScope&,
                             std::weak_ptr<AudacityProject> wProject)
        : mwProject{move(wProject)}
    {
        if (auto pProject = mwProject.lock()) {
            RealtimeEffectManager::Get(*pProject).ProcessStart(mSuspended);
        }
    }

    ProcessingScope(ProcessingScope&& other) = default;
    ProcessingScope& operator=(ProcessingScope&& other) = default;
    ~ProcessingScope()
    {
        if (auto pProject = mwProject.lock()) {
            RealtimeEffectManager::Get(*pProject).ProcessEnd(mSuspended);
        }
    }

    //! @return how many samples to discard for latency
    size_t Process(const ChannelGroup* group,
                   float* const* buffers,
                   float* const* scratch,
                   float* dummy,
                   unsigned nBuffers, //!< how many buffers; equal number of scratches
                   size_t numSamples //!< length of each buffer
                   )
    {
        if (const auto pProject = mwProject.lock()) {
            return RealtimeEffectManager::Get(*pProject)
                   .Process(mSuspended, group, buffers, scratch, dummy,
                            nBuffers, numSamples);
        }
        return 0; // consider them trivially processed
    }

private:
    std::weak_ptr<AudacityProject> mwProject;
    bool mSuspended{};
};
}

#endif
